Introduction to PIL/Pillow

Background Information

PIL stands for the Python Imaging Library. PIL hasn't been developed since 2009, but a gentleman named Alex Clark and some collaborators created a fork of PIL called "Pillow". Pillow is an evolved, more robust iteration of PIL.

So what does Pillow do?

PIL/Pillow provides image processing capabilities within Python. You can use PIL/Pillow to create and modify images, much as you would in Photoshop.

Pillow also provides resources for archival and batch editing purposes.

How might Pillow be useful for research?

If you are working with a large archive of still images, Pillow would provide you the capability to prepare your archive for analysis, but primarily, it seems to be of most value as a resource for preparing images and thumbnails to be displayed on a website or other display device. If you need to develop a repository of the visual material you are studying, Pillow would help you organize, batch edit, and present such data for your audience.

Pillow also provides tools for analyzing images to determine their composition down to the level of the pixel, which might be useful for some research!

While you can create images within Pillow, there are other modules and programming languages that are much more versatile for this type of task.

How to obtain Pillow:

Please note: You cannot have both PIL and Pillow installed on your system simultaneously. Uninstall PIL before installing Pillow.

If you have Anaconda, then you are all set.

Otherwise, use pip:

$ pip install Pillow

If you experience issues during installation, reference the Pillow Handbook for assistance.

Make an Image

Let's begin with the simple task of creating an image that is a simple block of color.

In [31]:
# First, we import our library. 

from PIL import Image

# We then create a new variable and assign it the values we wish our image to possess.
# RGB designates the color mode.
# The next tuple designates width and height in pixels.
# The final tuple designates color using the hexadecimal system. 
# All zeros results in black. All 255s results in white.

ourImage = Image.new('RGB', (150,100), (200,127,127))

# In order to view our image, we need to save it to the hard drive.

ourImage.save('hexadecimal.png')

hexadecimal.png

You can also designate color using certain recognized words.

In [8]:
ourImage2 = Image.new('RGB', (150,100), 'chartreuse')
ourImage2.save('allchartreuse.png')

allchartreuse.png

Import an Image

For research, however, you are more likely to be using other people's images. To import an image into Python, this is quite simple:

In [ ]:
from PIL import Image

ourImage3 = Image.open('directory/FileName.png')

Image Manipulation

Whether you create an image from scratch or upload an image, you can make changes to any image at the level of the pixel.

In [11]:
from PIL import Image

#First, I created a black square 100 pixels in width.
allblack = Image.new('RGB', (100,100), (0,0,0))

#Then I used the putpixel function to select a particular pixel and convert it white.
allblack.putpixel((50,50),(255,255,255))

#As always, I must then save this image in order to view it.
allblack.save('allblack.png')

allblack.png

In a similar fashion, using the loop function in Python, I can manipulate multiple pixels at the same time.

In [12]:
baseImage = Image.new('RGB', (100,100), (255,255,255))

for i in range(100):
    baseImage.putpixel((i,50),(0,0,0))
    
baseImage.save('baseImage.png')

baseImage.png

By using two nested loops, we can modify a series of pixels along both axes:

In [ ]:
testerImage = Image.new('RGB', (100,100), (0,0,0))
for x in range(50):
    for y in range (51,100):
        testerImage.putpixel((x,y),(255,255,255))
testerImage.save('testerwhite.png')

testerwhite.png

We can get even more advanced by associating our hexadecimal system with the size variables. In the following code, we set the color of each pixel to match the y axis (i.e. height) of the image. The x axis remains constant thus creating rows of color that begin as black (0) but gradually lighten to white (255).

In [32]:
rectangle = Image.new('RGB', (150,100), (0,0,0))
for x in range(150):
    for y in range(100):
        rectangle.putpixel((x,y),(y,y,y))
        
rectangle.save('gradient.png')

gradient.png

Working with Unfamiliar Images:

We can do the same kind of pixel manipulations using found images. For the purposes of this tutorial, I selected a black and white still from one of my favorite films, Federico Fellini's La Dolce Vita:

laDolceVita.png

When working with new images, some of your initial code will need to determine the parameters of the new images.

To determine the size tupel, use: pngimage.size(pngimage)

To determine the color of any particular pixel, the getpixel function is handy: pngimage.getpixel(x,y)

Using the getpixel function on an image such as the Fellini screenshot above, we quickly discover that many images contain 4 not 3 digits in the color designation tupel. While the first 3 digits reference the RGB hexadecimal assignments, the 4th digit designates transparency.

How to Lighten an Image:

To lighten images, we will construct our own function.

We designate our ranges using the size determination function and isolating the width and height. We then use the getpixel function to identify the color of each pixel. We will need to specify that we are only interested in the first RGB digits of the tupel. Having obtained the RGB values, we now add to each value to move the new values closer to (255,255,255) or white. We conclude by reuniting the tupel with its transparency value.

It looks like this:

In [17]:
def lighten(pngimage):
        for x in range(pngimage.size[0]):
        for y in range(pngimage.size[1]):
            (r,g,b) = pngimage.getpixel((x,y))[:3]
            new_color = (r+64, g+64, b+64)
            new_color = new_color + pngimage.getpixel((x,y))[3:]
            pngimage.putpixel((x,y), new_color)

If we were to apply to this our Fellini still, the code would look like this:

In [ ]:
ourImage4 = Image.open('python/laDolceVita.png')
lighten(ourImage)
ourImage.save('lighterfellini.png')

And the resulting image would look like this:

lighterfellini.png

In a similar fashion, you could darken the original image by lowering the numeric RGB value:

In [ ]:
def darken(pngimage):
    for x in range(pngimage.size[0]):
        for y in range(pngimage.size[1]):
            (r,g,b) = pngimage.getpixel((x,y))[:3]
            new_color = (r-34, g-34, b-34)
            new_color = new_color + pngimage.getpixel((x,y))[3:]
            pngimage.putpixel((x,y), new_color)
            
darkenImage = Image.open('python/laDolceVita.png')
darken(darkenImage)
darkenImage.save('darkerfellini.png')

darkerfellini.png

Seeing how one can construct the lighten and darken functions using Pillow provides a sense of the types of manipulations one can apply to images.

Other possible functions include:

  • Increasing contrast, using IF/THEN to darken dark pixels and lighten light pixels
  • Flipping an image, by swapping pixels
  • Adjusting the hue of an image, e.g. making it more or less red

While you can create these functions from scratch, many functions have already been developed within Pillow. Reference the handbook to see what is currently available.

How to Batch Edit Images

In [30]:
# We begin by defining our function. I'll use one we already covered.

def darken(pngimage):
    for x in range(pngimage.size[0]):
        for y in range(pngimage.size[1]):
            (r,g,b) = pngimage.getpixel((x,y))[:3]
            new_color = (r-34, g-34, b-34)
            new_color = new_color + pngimage.getpixel((x,y))[3:]
            pngimage.putpixel((x,y), new_color)

# We are now going to use another Python module to examine our directory and locate all .png files.

import glob
file_list = glob.glob('python/*.png')

# At this point, if we were to print file_list, we would get a list of filenames ending in .png from that directory.

# The next step is to create a loop that will iterate through all images in this directory.

for filename in file_list:
    current = Image.open(filename)
    darken(current)
    current.save(filename)
    
# This method will save over the original image, so make sure you have backups before proceeding. 

Helpful Extra Resources:

This tutorial provides a basic introduction to the manipulation of pixels within Python, but I would recommend using the handbook to learn the particular skills you need for your project. If you would like a more in-depth step-by-step tutorial, I recommend Nick Monfort's book (see below) as a good place to start, but Python for Beginners will provide some more tutorial options.